package com.intelligent.bot.utils.mj;

import cn.hutool.core.exceptions.ValidateException;
import com.intelligent.bot.base.exception.E;
import lombok.extern.slf4j.Slf4j;

import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Date;
import java.util.concurrent.ThreadLocalRandom;

@Slf4j
public class SnowFlake {
    private long workerId;
    private long datacenterId;
    private long sequence = 0L;
    private final long twepoch;
    private final long sequenceMask;
    private final long workerIdShift;
    private final long datacenterIdShift;
    private final long timestampLeftShift;
    private long lastTimestamp = -1L;
    private final boolean randomSequence;
    private long count = 0L;
    private final long timeOffset;
    private final ThreadLocalRandom tlr = ThreadLocalRandom.current();

    public static final SnowFlake INSTANCE = new SnowFlake();

    private SnowFlake() {
        this(false, 10, null, 5L, 5L, 12L);
    }

    private SnowFlake(boolean randomSequence, long timeOffset, Date epochDate, long workerIdBits, long datacenterIdBits, long sequenceBits) {
        if (null != epochDate) {
            this.twepoch = epochDate.getTime();
        } else {
            // 2012/12/12 23:59:59 GMT
            this.twepoch = 1355327999000L;
        }
        long maxWorkerId = ~(-1L << workerIdBits);
        long maxDatacenterId = ~(-1L << datacenterIdBits);
        this.sequenceMask = ~(-1L << sequenceBits);
        this.workerIdShift = sequenceBits;
        this.datacenterIdShift = sequenceBits + workerIdBits;
        this.timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
        this.randomSequence = randomSequence;
        this.timeOffset = timeOffset;
        try {
            this.datacenterId = getDatacenterId(maxDatacenterId);
            this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
        } catch (Exception e) {
            log.warn("datacenterId or workerId generate error: {}, set default value", e.getMessage());
            this.datacenterId = 4;
            this.workerId = 1;
        }
    }

    public synchronized String nextId() {
        long currentTimestamp = timeGen();
        if (currentTimestamp < this.lastTimestamp) {
            long offset = this.lastTimestamp - currentTimestamp;
            if (offset > this.timeOffset) {
                throw new ValidateException("Clock moved backwards, refusing to generate id for [" + offset + "ms]");
            }
            try {
                this.wait(offset << 1);
            } catch (InterruptedException e) {
                throw new E(e.getMessage());
            }
            currentTimestamp = timeGen();
            if (currentTimestamp < this.lastTimestamp) {
                throw new E("Clock moved backwards, refusing to generate id for [" + offset + "ms]");
            }
        }
        if (this.lastTimestamp == currentTimestamp) {
            long tempSequence = this.sequence + 1;
            if (this.randomSequence) {
                this.sequence = tempSequence & this.sequenceMask;
                this.count = (this.count + 1) & this.sequenceMask;
                if (this.count == 0) {
                    currentTimestamp = this.tillNextMillis(this.lastTimestamp);
                }
            } else {
                this.sequence = tempSequence & this.sequenceMask;
                if (this.sequence == 0) {
                    currentTimestamp = this.tillNextMillis(lastTimestamp);
                }
            }
        } else {
            this.sequence = this.randomSequence ? this.tlr.nextLong(this.sequenceMask + 1) : 0L;
            this.count = 0L;
        }
        this.lastTimestamp = currentTimestamp;
        long id = ((currentTimestamp - this.twepoch) << this.timestampLeftShift) |
                (this.datacenterId << this.datacenterIdShift) |
                (this.workerId << this.workerIdShift) |
                this.sequence;
        return String.valueOf(id);
    }

    private static long getDatacenterId(long maxDatacenterId) {
        long id = 0L;
        try {
            InetAddress ip = InetAddress.getLocalHost();
            NetworkInterface network = NetworkInterface.getByInetAddress(ip);
            if (network == null) {
                id = 1L;
            } else {
                byte[] mac = network.getHardwareAddress();
                if (null != mac) {
                    id = ((0x000000FF & (long) mac[mac.length - 1]) | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
                    id = id % (maxDatacenterId + 1);
                }
            }
        } catch (Exception e) {
            throw new E(e.getMessage());
        }
        return id;
    }

    private static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
        StringBuilder macIpPid = new StringBuilder();
        macIpPid.append(datacenterId);
        try {
            String name = ManagementFactory.getRuntimeMXBean().getName();
            if (name != null && !name.isEmpty()) {
                macIpPid.append(name.split("@")[0]);
            }
            String hostIp = InetAddress.getLocalHost().getHostAddress();
            String ipStr = hostIp.replace("\\.", "");
            macIpPid.append(ipStr);
        } catch (Exception e) {
            throw new E(e.getMessage());
        }
        return (macIpPid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
    }

    private long tillNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    private long timeGen() {
        return System.currentTimeMillis();
    }
}
